# encoding: utf-8
#Copyright 2008 Adam A. Crossland
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
#http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.

from google.appengine.ext import db
from google.appengine.ext import gql
from google.appengine.ext.db import GqlQuery
import string
import logging

class Tag(db.Model):
    "Google AppEngine model for store of tags."
    tag = db.StringProperty(required = True)
    "The actual string value of the tag."
    added = db.DateTimeProperty(auto_now_add = True)
    "The date and time that the tag was first added to the datastore."
    tagged = db.ListProperty(db.Key)
    "A List of db.Key values for the datastore objects that have been tagged with this tag value."
    
    def get_tags_for(key):
        "Set the tags for the datastore object represented by key."
        tags = GqlQuery("SELECT * FROM Tag WHERE tagged = :1", key)
        
        return tags
    Get_Tags_For = staticmethod(get_tags_for)
    
    def get_by_tag_value(tag_value):
        "Get the Tag object that has the tag value given by tag_value."
        # There should only ever be one record in the datastore for a given tag value.
        found_tag = Tag.gql('WHERE tag = :1', tag_value).fetch(1)
        if len(found_tag) == 0:
            # The given tag is not yet in the datastore.
            found_tag = None
        else:
            # Even if there is only one value, fetch still returns a List, so we have to
            # pick element 0 in order to return an actual Tag instead of a List.
            found_tag = found_tag[0]
            
        return found_tag
    Get_By_Tag_Value = staticmethod(get_by_tag_value)
        
    def set_tags_for(key, tag_list):
        "Apply the tags in the List tag_list to the datastore object represented by key."
        for each_tag in tag_list:
            # Make sure that we don't have any leading or trailing whitespace
            each_tag = string.strip(each_tag)
            existing_tag = Tag.gql("WHERE tag = :1", each_tag).fetch(1)
            if len(existing_tag) == 0:
                # This tag is not yet in the datastore, so create it, add the key and save it.
                tagged_list = []
                tagged_list.append(key)
                new_tag = Tag(tag = each_tag, tagged = tagged_list)
                new_tag.put()
            else:
                # The tag already exists, add the new key and save.
                existing_tag = existing_tag[0]
                if key not in existing_tag.tagged:
                    existing_tag.tagged.append(key)
                    existing_tag.save()
    Set_Tags_For = staticmethod(set_tags_for)
    
    def get_tags_by_frequency(limit = 0):
        """Return a list of Tags sorted by the number of objects to which they have been applied,
        most frequently-used first.  If limit is given, return only that many tags; otherwise,
        return all."""
        
        tag_list = Tag.all().fetch(1000)
        # Even if the caller has supplied a limit, we need to get all of the tags and
        # sort them, as they won't come from the datastore already ordered by frequency
        # of use.
        ntag_list=[]
        for tag in tag_list:
            if len(tag.tagged)>0:
                ntag_list.append(tag)
        ntag_list.sort(sort_by_frequency)
        if limit > 0:
            ntag_list = copy(ntag_list[0:limit])
        return ntag_list
    Get_Tags_By_Frequency = staticmethod(get_tags_by_frequency)

def sort_by_frequency(tag_a, tag_b):
    comp = 0 # Assume equality
    if len(tag_a.tagged) > len(tag_b.tagged):
        comp = -1
    elif len(tag_a.tagged) < len(tag_b.tagged):
        comp = 1
        
    return comp

class Taggable:
    """A mixin class that is used for making Google AppEnigne Model classes taggable.
        Usage:
            class Post(db.Model, taggable.Taggable):
                body = db.TextProperty(required = True)
                title = db.StringProperty()
                added = db.DateTimeProperty(auto_now_add=True)
                edited = db.DateTimeProperty()
            
                def __init__(self, parent=None, key_name=None, app=None, **entity_values):
                    db.Model.__init__(self, parent, key_name, app, **entity_values)
                    taggable.Taggable.__init__(self)
    """
    
    def __init__(self):
        self.tags = None
        "A List of the tags for this object.  Populated by get_tags()."
        self.tag_string = ""
        "A string representation of the list of tags for this object.  Populated by get_tags_as_string()."
        self.tag_seperator = ","
        "The string that is used to separate individual tags in a string representation of a list of tags."

    def get_tags(self):
        "Get a List of Tag objects for all Tags that apply to this object."
        tags_list = Tag.Get_Tags_For(self.key())
        if tags_list != None:
            self.tags = []
            for each_tag in tags_list:
                self.tags.append(each_tag)
        return self.tags
    
    def get_tags_as_string(self):
        "Create a string representation of all of the tags that apply to this object."
        if self.tags == None:
            # We build the string using the tags property, so we need to make sure that it has been populated.
            self.get_tags()
        tags_as_string = ""
        if self.tags != None:
            for each_tag in self.tags:
                if len(tags_as_string) != 0:
                    tags_as_string += self.tag_seperator
                tags_as_string += each_tag.tag
            self.tag_string = tags_as_string
                
        return tags_as_string
    
    def __repr__(self):
        if (self.tag_string == None) or (len(self.tag_string) == 0):
            # If there isn't a value yet in tag_string, call get_tags_as_string to
            # populate it.  After the call, it might still be empty, but we know at least
            # that it is empty because there are no tags for this object.
            self.get_tags_as_string()
            
        return self.tag_string
    
    def set_tags(self, tag_list,append=True):
        if not append:
            del_tags_list = Tag.Get_Tags_For(self.key())
            if del_tags_list != None:
                for each_del_tag in del_tags_list:
                    each_del_tag.tagged.remove(self.key())
                    if len(each_del_tag.tagged) ==0:
                        each_del_tag.delete()
                    else:
                        each_del_tag.put()
                        
        "Set the tags for the Taggable object from a list of strings."
        for each_tag in tag_list:
            # We need to call strip on each tag string, as leading and trailing
            # whitespace is very likely. We don't want to end up with
            # tags that are dupes of others because they have whitespace.
            each_tag = string.strip(each_tag)
            # It is possible that we could receive a tag_list array that has an empty member;
            # this could happen because of two consecutive seperators or a list that ends
            # with a seperator.  Check and skip processing such list elements.
            if len(each_tag) > 0:
                existing_tag = Tag.Get_By_Tag_Value(each_tag)
                if existing_tag == None:
                    # This tag does not yet exist in the datastore, so create it,
                    # set the tags and put it in the store.
                    tagged_list = []
                    tagged_list.append(self.key())
                    new_tag = Tag(tag = each_tag, tagged = tagged_list)
                    new_tag.put()
                else:
                    # This tag is already in the datastore.  If the object is not
                    # already in the tagged list, add it.  Otherwise, do nothing;
                    # we're good!
                    if (self.key() not in existing_tag.tagged):
                        existing_tag.tagged.append(self.key())
                        existing_tag.save()
        
    def set_tags_from_string(self, tag_list,append=True):
        """Set the tags for the Taggable object from a string that contains one or more
        tags seperated by the character or charaters in self.tag_seperator."""
        tags = tag_list.split(self.tag_seperator)
        self.set_tags(tags,append)